use std::collections::BTreeMap;
use std::fmt::Display;
use std::fs::File;
use std::path::Path;
use std::path::PathBuf;

use serde::Deserialize;
use serde::Serialize;

type Result<T> = std::result::Result<T, ()>;

use clap::{Args, Parser};

#[derive(Args)]
struct LsOpt {
    #[clap(default_value = "/")]
    path: String,

    /// Show direcotry instead of list content of it.
    #[clap(short, long)]
    directory: bool,

    /// Show content of directory recursively.
    #[clap(short, long)]
    recursive: bool,
}

#[derive(Parser)]
enum Cli {
    /// Show name of file or directory.
    Ls(LsOpt),

    /// Copy a file.
    Cp {
        src: PathBuf,
        dst: String,

        /// Override target file if exists.
        #[clap(short, long)]
        force: bool,
    },

    /// Rename a file.
    Mv {
        src: String,
        dst: String,

        /// Override target file if exists.
        #[clap(short, long)]
        force: bool,
    },
}

#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum QTree {
    File(usize),
    Dir(BTreeMap<String, QTree>),
}

impl QTree {
    fn find_subtree<'t>(&'t self, path: &QPath) -> Result<&'t QTree> {
        let mut tree = self;

        for comp in &path.components {
            let QTree::Dir(btree_map) = tree else {
                eprintln!("quanfs: {path} not exists");
                return Err(());
            };

            let Some(v) = btree_map.get(comp) else {
                eprintln!("quanfs: {comp} not exists");
                return Err(());
            };

            tree = v;
        }

        Ok(tree)
    }

    fn find_subtree_mut<'t>(&'t mut self, path: &QPath) -> Result<&'t mut QTree> {
        let mut tree = self;

        for comp in &path.components {
            let QTree::Dir(btree_map) = tree else {
                eprintln!("quanfs: {path} not exists");
                return Err(());
            };

            let Some(v) = btree_map.get_mut(comp) else {
                eprintln!("quanfs: {comp} not exists");
                return Err(());
            };

            tree = v;
        }

        Ok(tree)
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Env {
    tree: QTree,
    cwd: Vec<String>,
    file_count: usize,
}

struct QPath {
    components: Vec<String>,
    is_dir: bool,
}

impl Display for QPath {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let Some((first, rest)) = self.components.split_first() else {
            return Ok(());
        };

        write!(f, "/{first}")?;

        for each in rest {
            write!(f, "/{each}")?;
        }

        if self.is_dir {
            write!(f, "/")?;
        }

        Ok(())
    }
}

impl QPath {
    fn new(components: Vec<String>, is_dir: bool) -> Self {
        Self { components, is_dir }
    }

    fn split_file_name(&self) -> Result<(QPath, String)> {
        if self.is_dir {
            return Err(());
        }

        let (last, rest) = self.components.split_last().unwrap();

        Ok((
            QPath {
                is_dir: true,
                components: rest.to_owned(),
            },
            last.to_owned(),
        ))
    }
}

impl Env {
    fn dump(&self) -> Result<()> {
        let writer = File::create("root.d/env.json").unwrap();
        serde_json::to_writer_pretty(writer, self).unwrap();
        Ok(())
    }

    fn normalize(&self, path: &str) -> QPath {
        if path.is_empty() {
            return QPath::new(self.cwd.clone(), true);
        }

        let mut buf = Vec::new();

        if !path.starts_with('/') {
            buf.extend(self.cwd.clone());
        }

        for each in path.split('/') {
            if each.is_empty() {
                continue;
            }

            buf.push(each.to_owned());
        }

        QPath::new(buf, path.ends_with('/'))
    }
}

fn walk(parent_path: &mut QPath, name: &str, tree: &QTree) {
    match tree {
        QTree::File(_) => {
            parent_path.is_dir = false;
            parent_path.components.push(name.to_string());

            println!("{}", parent_path);

            parent_path.is_dir = true;
            parent_path.components.pop();
            return;
        }
        QTree::Dir(btree_map) => {
            parent_path.components.push(name.to_string());

            for (name, tree) in btree_map.iter() {
                walk(parent_path, name, tree)
            }

            parent_path.components.pop();
        }
    }
}

fn ls_main(env: &mut Env, ls_opt: &LsOpt) -> Result<()> {
    let mut path = env.normalize(&ls_opt.path);
    let tree = env.tree.find_subtree(&path)?;

    match tree {
        QTree::File(_) => {
            if path.is_dir {
                eprintln!("ls: {} is a file, not a dir", path);
                return Err(());
            }

            println!("{}", path);
        }
        QTree::Dir(btree_map) => {
            if !path.is_dir {
                eprintln!("ls: {} is a dir, not a file", path);
                return Err(());
            }

            if ls_opt.directory {
                println!("{}", path);
            } else if ls_opt.recursive {
                // let mut parent_path = QPath::new(vec![], true);

                for (name, tree) in btree_map.iter() {
                    walk(&mut path, name, tree);
                }
            } else {
                for name in btree_map.keys() {
                    println!("{}", name);
                }
            }
        }
    }

    Ok(())
}

fn main() {
    if runtime().is_err() {
        std::process::exit(1);
    }
}

fn runtime() -> Result<()> {
    let cli = Cli::parse();

    let path = Path::new("root.d/env.json");

    let mut env = if path.exists() {
        let reader = File::open(path).unwrap();
        serde_json::from_reader(reader).unwrap()
    } else {
        Env {
            tree: QTree::Dir(BTreeMap::new()),
            cwd: vec!["/home/".to_owned()],
            file_count: 0,
        }
    };

    match cli {
        Cli::Ls(opt) => {
            ls_main(&mut env, &opt)?;
        }
        Cli::Cp { src, dst, force } => {
            cp_main(&mut env, &src, &dst, force)?;
        }
        Cli::Mv { .. } => {
            // let src = env.normalize(&src);
            // let dst = env.normalize(&dst);
            //
            // let Some(src_id) = env.tree.remove(&src) else {
            //     eprintln!("mv: source file not exists: {src}");
            //     return Err(());
            // };
            //
            // match env.tree.entry(dst) {
            //     std::collections::btree_map::Entry::Vacant(vacant_entry) => {
            //         vacant_entry.insert(src_id);
            //     }
            //     std::collections::btree_map::Entry::Occupied(mut occupied_entry) => {
            //         if !force {
            //             eprintln!("cp: target exists: {}", occupied_entry.key());
            //             return Err(());
            //         }
            //         *occupied_entry.get_mut() = src_id;
            //     }
            // }
            //
            // env.dump()?;
        }
    }

    Ok(())
}

fn cp_main(env: &mut Env, src: &Path, dst: &str, force: bool) -> Result<()> {
    let dst = env.normalize(dst);

    let (dst_dir, dst_file) = if dst.is_dir {
        (dst, src.file_name().unwrap().to_string_lossy().to_string())
    } else {
        dst.split_file_name()?
    };

    let tree = env.tree.find_subtree_mut(&dst_dir)?;

    let tree_map = match tree {
        QTree::File(_) => {
            eprintln!("cp: target directory is a file");
            return Err(());
        }
        QTree::Dir(btree_map) => btree_map,
    };

    let n = env.file_count;

    match tree_map.entry(dst_file) {
        std::collections::btree_map::Entry::Vacant(vacant_entry) => {
            let dst_fs = blob_path(n);
            std::fs::copy(src, &dst_fs).map_err(|err| eprintln!("cp: failed to copy: {err}"))?;
            vacant_entry.insert(QTree::File(n));
            env.file_count += 1;
        }
        std::collections::btree_map::Entry::Occupied(occupied_entry) => {
            if !force {
                eprintln!("cp: target exists: {}", occupied_entry.key());
                return Err(());
            }

            let id = match occupied_entry.get() {
                QTree::File(id) => id,
                QTree::Dir(_) => {
                    eprintln!("cp: target is directory");
                    return Err(());
                }
            };

            let dst_fs = blob_path(*id);
            std::fs::copy(src, &dst_fs).map_err(|err| eprintln!("cp: failed to copy: {err}"))?;
        }
    }

    env.dump()?;

    Ok(())
}

fn blob_path(n: usize) -> PathBuf {
    let dst_fs = Path::new("root.d")
        .join("blob")
        .join(format!("{n:06}.blob"));
    dst_fs
}
